﻿using System;
using System.Linq;
using Notoric.Commanding;
using Notoric.Commanding.CommandBase;
using Notoric.Model.Utils;

namespace Notoric.ViewModel
{
    /// <summary>
    /// An undoable command that performs an automatic layout of a music section interior.
    /// </summary>
    /// <remarks>
    /// Input Parameters:
    /// <list type="table">
    /// <listheader>
    /// <term>
    ///     Name
    /// </term>
    /// <description>
    ///     Description
    /// </description>
    /// <item>
    /// <term>
    ///     <see cref="Interior"/> (<see cref="Elements.CWNotation.MusicSectionInterior"/>)
    /// </term>
    /// <description>
    ///     The music section interior to layout.
    /// </description>
    /// </item>
    /// </listheader>
    /// </list>
    /// </remarks>
    public class MusicSectionInteriorLayoutCommand : UndoableCommandBase
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="MusicSectionInteriorLayoutCommand"/> class.
        /// </summary>
        /// <param name="undoStack">The undo command stack</param>
        /// <param name="redoStack">The redo command stack</param>
        public MusicSectionInteriorLayoutCommand(CommandStack undoStack, CommandStack redoStack)
            : base(undoStack, redoStack)
        {
            AddParameter<Elements.CWNotation.MusicSectionInterior>("Interior", ParameterDirection.In);
            Name = "Layout";

            verticalLayoutSubCommand = new UndoableMacroCommand(undoStack, redoStack)
            {
                UseStacks = false
            };
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Gets the highest position occupied by a staff symbol.
        /// </summary>
        /// <param name="staff">
        /// <para>
        /// Type: <see cref="Elements.CWNotation.Staff"/>
        /// </para>
        /// <para>
        /// The staff.
        /// </para>
        /// </param>
        /// <param name="staffTop">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The position of the top-most staff line.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The vertical position of the highest symbol of the staff.
        /// </para>
        /// </returns>
        private double GetHighestPosition(Elements.CWNotation.Staff staff, double staffTop)
        {
            var highestSymbol = (from symbol in staff.Symbols
                                 where symbol is Elements.CWNotation.DurationalSymbol || symbol is Elements.CWNotation.Clef
                                 select symbol).TakeMax(s => s.StaffPosition);

            var verticalPosition = staffTop + staff.Height - highestSymbol.StaffPosition * (staff.SpaceSize / 2);
            if (highestSymbol is Elements.CWNotation.Clef)
            {
                var clef = (Elements.CWNotation.Clef)highestSymbol;
                switch (clef.Type)
                {
                    case Elements.CWNotation.ClefType.G:
                        verticalPosition -= 1.25 * highestSymbol.EffectiveTypeSize;
                        break;
                    case Elements.CWNotation.ClefType.F:
                        verticalPosition -= highestSymbol.EffectiveTypeSize / 4;
                        break;
                    case Elements.CWNotation.ClefType.C:
                        verticalPosition -= 0.5 * highestSymbol.EffectiveTypeSize;
                        break;
                }
            }
            
            return verticalPosition;
        }

        /// <summary>
        /// Gets the lowest position occupied by a staff symbol.
        /// </summary>
        /// <param name="staff">
        /// <para>
        /// Type: <see cref="Elements.CWNotation.Staff"/>
        /// </para>
        /// <para>
        /// The staff.
        /// </para>
        /// </param>
        /// <param name="staffTop">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The position of the top-most staff line.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The vertical position of the lowest symbol of the staff.
        /// </para>
        /// </returns>
        private double GetLowestPosition(Elements.CWNotation.Staff staff, double staffTop)
        {
            var lowestSymbol = (from symbol in staff.Symbols
                                 where symbol is Elements.CWNotation.DurationalSymbol || symbol is Elements.CWNotation.Clef
                                 select symbol).TakeMin(s => s.StaffPosition);

            var verticalPosition = staffTop + staff.Height - lowestSymbol.StaffPosition * (staff.SpaceSize / 2);
            if (lowestSymbol is Elements.CWNotation.Clef)
            {
                var clef = (Elements.CWNotation.Clef)lowestSymbol;
                switch (clef.Type)
                {
                    case Elements.CWNotation.ClefType.G:
                        verticalPosition += 0.75 * lowestSymbol.EffectiveTypeSize;
                        break;
                    case Elements.CWNotation.ClefType.F:
                        verticalPosition += 0.5 * lowestSymbol.EffectiveTypeSize;
                        break;
                    case Elements.CWNotation.ClefType.C:
                        verticalPosition += 0.5 * lowestSymbol.EffectiveTypeSize;
                        break;
                }
            }

            return verticalPosition;
        }

        /// <summary>
        /// Layouts the paged interior.
        /// </summary>
        private void LayoutPagedInterior()
        {
            double basicStavesDistance = 1.0;       // Basic distance between two staves of the same part (in free space units)
            double basicPartsDistance = 1.1;        // Basic distance between two staves of different parts (in free space units)
            double basicSystemsDistance = 1.3;      // Basic distance between two systems (in free space units)

            double freeSpaceUnits = 0.0;            // Total number of free space units needed
            double occupiedVSpace = 0.0;            // Total occupied space in the vertical direction (in centimeters)

            foreach (var system in Interior.Systems)
            {
                if (system.Staves.Count > 0)
                {
                    var lastPart = ((Model.Elements.CWNotation.Staff)system.Staves[0].AdaptedElement).Descriptor.FindOwner<Model.Elements.PartDescriptorElement>();

                    foreach (var staff in system.Staves)
                    {
                        occupiedVSpace += staff.Height;

                        if (((Model.Elements.CWNotation.Staff)staff.AdaptedElement).Descriptor.FindOwner<Model.Elements.PartDescriptorElement>() == lastPart)
                            freeSpaceUnits += basicStavesDistance;
                        else
                        {
                            freeSpaceUnits += basicPartsDistance;
                            lastPart = ((Model.Elements.CWNotation.Staff)staff.AdaptedElement).Descriptor.FindOwner<Model.Elements.PartDescriptorElement>();
                        }
                    }

                    freeSpaceUnits += basicSystemsDistance;
                }
            }

            var section = Interior.FindOwner<Elements.MusicSection>();
            double totalVSpace = section.Height - section.Margin.Top - section.Margin.Bottom - occupiedVSpace;
            double freeSpaceUnit = totalVSpace / freeSpaceUnits;

            double actualTop = section.Margin.Top + basicPartsDistance * freeSpaceUnit;

            foreach (var system in Interior.Systems)
            {
                verticalLayoutSubCommand.Commands.Add(system.CreateSetTopCommand(actualTop));

                if (system.Staves.Count > 0)
                {
                    var lastPart = ((Model.Elements.CWNotation.Staff)system.Staves[0].AdaptedElement).Descriptor.FindOwner<Model.Elements.PartDescriptorElement>();
                    double actualStaffTop = 0.0;

                    foreach (var staff in system.Staves)
                    {
                        verticalLayoutSubCommand.Commands.Add(staff.CreateSetTopCommand(actualStaffTop));
                        actualStaffTop += staff.Height;

                        if (((Model.Elements.CWNotation.Staff)staff.AdaptedElement).Descriptor.FindOwner<Model.Elements.PartDescriptorElement>() == lastPart)
                            actualStaffTop += basicStavesDistance * freeSpaceUnit;
                        else
                        {
                            actualStaffTop += basicPartsDistance * freeSpaceUnit;
                            lastPart = ((Model.Elements.CWNotation.Staff)staff.AdaptedElement).Descriptor.FindOwner<Model.Elements.PartDescriptorElement>();
                        }
                    }

                    verticalLayoutSubCommand.Commands.Add(system.CreateSetHeightCommand(actualStaffTop));                    
                    actualTop += actualStaffTop + basicSystemsDistance * freeSpaceUnit;
                }
            }

            verticalLayoutSubCommand.Execute(null);

            foreach (var system in Interior.Systems)
                Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(system.AdaptedElement as Model.Elements.CWNotation.System);
        }

        /// <summary>
        /// Layouts the linear view.
        /// </summary>
        private void LayoutLinearView()
        {
            if (Interior.Systems.Count == 0)
                return;
            var system = Interior.Systems[0];

            UndoableCommandBase systemLeft = null;
            if (system.StaffConnectors.Count > 0)
                systemLeft = system.CreateSetLeftCommand(18);
            else
                systemLeft = system.CreateSetLeftCommand(0);
            verticalLayoutSubCommand.Commands.Add(systemLeft);

            double actualTop = 87;
            Model.Elements.PartDescriptorElement actualPart = null;
            var systemTop = system.CreateSetTopCommand(0);
            verticalLayoutSubCommand.Commands.Add(systemTop);

            foreach (var staff in system.Staves)
            {
                if (((Model.Elements.CWNotation.Staff)staff.AdaptedElement).Descriptor.FindOwner<Model.Elements.PartDescriptorElement>() != actualPart)
                {
                    actualTop += 9;
                    actualPart = ((Model.Elements.CWNotation.Staff)staff.AdaptedElement).Descriptor.FindOwner<Model.Elements.PartDescriptorElement>();
                }

                var highestPos = GetHighestPosition(staff, actualTop);
                if (highestPos < actualTop)
                    actualTop += actualTop - highestPos;

                var cmd = staff.CreateSetTopCommand(actualTop);
                verticalLayoutSubCommand.Commands.Add(cmd);

                var lowestPos = GetLowestPosition(staff, actualTop);
                actualTop = Math.Max(actualTop + staff.Height, lowestPos) + 58;
            }

            verticalLayoutSubCommand.Commands.Add(system.CreateSetHeightCommand(actualTop));

            verticalLayoutSubCommand.Execute(null);

            Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(system.AdaptedElement as Model.Elements.CWNotation.System);
        }

        #endregion

        #region Command Logic

        /// <inheritdoc/>
        protected override void CommandOperation(object parameter)
        {
            if (Interior.FindOwner<Elements.Page>() == null)
                LayoutLinearView();
            else
                LayoutPagedInterior();

            Interior.FindOwner<Elements.Project>().IsModified = true;
        }

        /// <inheritdoc/>
        protected override void RedoOperation()
        {
            verticalLayoutSubCommand.Execute(null);

            if (Interior.FindOwner<Elements.Page>() == null)
                Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(Interior.Systems[0].AdaptedElement as Model.Elements.CWNotation.System);
            else
            {
                foreach (var system in Interior.Systems)
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(system.AdaptedElement as Model.Elements.CWNotation.System);
            }
        }

        /// <inheritdoc/>
        protected override void UndoOperation()
        {
            if (Interior.FindOwner<Elements.Page>() == null)
                Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(Interior.Systems[0].AdaptedElement as Model.Elements.CWNotation.System);
            else
            {
                foreach (var system in Interior.Systems)
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(system.AdaptedElement as Model.Elements.CWNotation.System);
            }

            verticalLayoutSubCommand.Execute(null);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the the music section interior to layout.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Elements.CWNotation.MusicSectionInterior"/>
        /// </para>
        /// <para>
        /// The music section interior to layout.<br/>
        /// This property wraps the "Interiro" command parameter.
        /// </para>
        /// </value>
        public Elements.CWNotation.MusicSectionInterior Interior
        {
            get { return GetParameterValue<Elements.CWNotation.MusicSectionInterior>("Interior"); }
            set { SetParameterValue<Elements.CWNotation.MusicSectionInterior>("Interior", value); }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the sub-command that performed the vertical layout.
        /// </summary>
        private UndoableMacroCommand verticalLayoutSubCommand;

        #endregion
    }
}
